// Copyright 2008 David Mitchell
// Released under the MIT License

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.InteropServices;

namespace NSDuctTape
{
	/// <summary>
	/// Registers .NET classes with the Objective C runtime.
	/// </summary>
	public static class Registrar
	{
		public static Type TypeForInstance(IntPtr instance)
		{
			IntPtr targetClass = ObjectiveCMethods.SendMessage(instance, Selectors.Class);

			return s_typesForWrappers[targetClass];
		}

		public static MethodBase MethodForSelector(Type type, string selector)
		{
			MethodBase result;
			s_selectorMethods[type].TryGetValue(selector, out result);
			return result;
		}

		public static PropertyInfo PropertyForKey(Type type, string key)
		{
			PropertyInfo info;
			s_keyProperties[type].TryGetValue(key, out info);
			return info;
		}

		public static IntPtr ClassForType(Type type)
		{
			IntPtr wrapper;
			if (!s_typeWrappers.TryGetValue(type, out wrapper))
			{
				string className = type.Name;
				Registrar.RegisterClass(className, type);
				wrapper = ObjectiveCMethods.GetClass(className);
			}

			return wrapper;
		}

		public static void RegisterClass(string className, Type type)
		{
			IntPtr baseClass;
			bool isList = (type.GetInterface("System.Collections.Generic.IList`1") ?? type.GetInterface("System.Collections.IList")) != null;
			if (isList)
				baseClass = Classes.MutableArray;
			else
				baseClass = Classes.Object;

			IntPtr newClass = ObjectiveCMethods.CreateClass(baseClass, className, 0);
			if (newClass == IntPtr.Zero)
			{
				int nAppend = 1;
				do
				{
					newClass = ObjectiveCMethods.CreateClass(baseClass, className + nAppend++, 0);
				} while (newClass == IntPtr.Zero);
			}
			
			IntPtr metaClass = ObjectiveCMethods.ClassForObject(newClass);
			AddMethod(metaClass, "respondsToSelector:", "RespondsToSelector", "c12@0:4:8");
			s_typesForWrappers[newClass] = type;

			int size = Marshal.SizeOf(typeof(IntPtr));
			byte alignment = (byte)Math.Log(size, 2.0);
			ObjectiveCMethods.AddField(newClass, "gcHandle", size, alignment, "@");
			ObjectiveCMethods.AddField(newClass, "notifyHandle", size, alignment, "@");

			AddMethod(newClass, "respondsToSelector:", "RespondsToSelector", "c12@0:4:8");
			AddMethod(newClass, "forwardInvocation:", "ForwardInvocation");
			AddMethod(newClass, "methodSignatureForSelector:", "MethodSignatureForSelector");
			AddMethod(newClass, "dealloc", "Dealloc");
			AddMethod(newClass, "valueForKey:", "ValueForKey");
			AddMethod(newClass, "mutableArrayValueForKey:", "MutableArrayValueForKey");
			AddMethod(newClass, "setValue:forKey:", "SetValueForKey");

			if (isList)
			{
				AddMethod(newClass, "count", "Count");
				AddMethod(newClass, "objectAtIndex:", "ObjectAtIndex");
				AddMethod(newClass, "insertObject:atIndex:", "InsertObjectAtIndex");
				AddMethod(newClass, "removeObjectAtIndex:", "RemoveObjectAtIndex");
				AddMethod(newClass, "addObject:", "AddObject");
				AddMethod(newClass, "removeLastObject", "RemoveLastObject");
				AddMethod(newClass, "replaceObjectAtIndex:withObject:", "ReplaceObjectAtIndexWithObject");
			}

			ConstructorInfo constructor = type.GetConstructor(Type.EmptyTypes);
			if (constructor != null)
				AddMethod(newClass, "init", "Init");

Console.WriteLine("Registering class {0} for type {1}", newClass, type);
			ObjectiveCMethods.RegisterClass(newClass);

			Dictionary<string, MethodBase> selectors = new Dictionary<string, MethodBase>();
			foreach (MethodBase info in type.GetMethods(BindingFlags.Instance | BindingFlags.Public))
			{
				ObjectiveCNameAttribute attribute = MemberInfoUtility.GetCustomAttribute<ObjectiveCNameAttribute>(info);
				if (attribute != null)
				{
					selectors.Add(attribute.Name, info);
				}
				else
				{
					string selector = info.Name;
					foreach (ParameterInfo parameter in info.GetParameters())
						selector += string.Format("With{0}:", parameter.ParameterType.Name);
					selectors.Add(selector, info);
				}
			}

			foreach (MethodBase info in type.GetConstructors(BindingFlags.Public))
			{
				ObjectiveCNameAttribute attribute = MemberInfoUtility.GetCustomAttribute<ObjectiveCNameAttribute>(info);
				if (attribute != null)
				{
					selectors.Add(attribute.Name, info);
				}
				else
				{
					string selector = info.Name;
					foreach (ParameterInfo parameter in info.GetParameters())
						selector += string.Format("With{0}:", parameter.ParameterType.Name);
					selectors.Add(selector, info);
				}
			}
			s_selectorMethods.Add(type, selectors);


			Dictionary<string, PropertyInfo> properties = new Dictionary<string, PropertyInfo>();
			foreach (PropertyInfo info in type.GetProperties(BindingFlags.Instance | BindingFlags.Public))
			{
				ObjectiveCNameAttribute attribute = MemberInfoUtility.GetCustomAttribute<ObjectiveCNameAttribute>(info);
				properties.Add(attribute != null ? attribute.Name : info.Name, info);
			}
			s_keyProperties.Add(type, properties);

			s_typeWrappers.Add(type, newClass);
		}

		private static void AddMethod(IntPtr nativeClass, string objectiveCName, string methodName, string types)
		{
			IntPtr function = typeof(MethodImplementations).GetMethod(methodName, BindingFlags.Static | BindingFlags.NonPublic).MethodHandle.GetFunctionPointer();
			ObjectiveCMethods.AddMethod(nativeClass, ObjectiveCMethods.SelectorFromString(objectiveCName),
				typeof(MethodImplementations).GetMethod(methodName, BindingFlags.Static | BindingFlags.NonPublic).MethodHandle.GetFunctionPointer(), types);
		}

		private static void AddMethod(IntPtr nativeClass, string objectiveCName, string methodName)
		{
			AddMethod(nativeClass, objectiveCName, methodName, TypeStrings.ForMethod(typeof(MethodImplementations).GetMethod(methodName, BindingFlags.Static | BindingFlags.NonPublic)));
		}

		static readonly Dictionary<Type, Dictionary<string, MethodBase>> s_selectorMethods = new Dictionary<Type, Dictionary<string, MethodBase>>();
		static readonly Dictionary<Type, Dictionary<string, PropertyInfo>> s_keyProperties = new Dictionary<Type, Dictionary<string, PropertyInfo>>();
		static readonly Dictionary<Type, IntPtr> s_typeWrappers = new Dictionary<Type, IntPtr>();
		static readonly Dictionary<IntPtr, Type> s_typesForWrappers = new Dictionary<IntPtr, Type>();
	}
}
